
// To change this License template, choose Tools / Templates
// and edit Licenses / FanDefaultLicense.txt
//
// History:
//   07.09.2010 JSTAUDEMEYER Creation
//

using [java]javax.sound.midi
using [java]fanx.interop::ByteArray
using [java]java.lang::Thread

**
** MidiSequence
**
class SequenceBuilder
{

  static const Int RESOLUTION := 256/4
  static const Int WHOLE := RESOLUTION * 4

  static const Int META_END_OF_TRACK := 0x2f
  static const Int META_SET_TEMPO := 0x51
  static const Int META_TIME_SIGNATURE := 0x58
  static const Int META_KEY_SIGNATURE := 0x59
  static const Int DEFAULT_TEMPO := 120
  static const Int MAX_CHANNELS := 16
  static const Int PERC_CHANNEL := 9

  Sequence sequence := Sequence(Sequence.PPQ, RESOLUTION)

  private TrackBuilder[] trackBuilders := [,]
  private Int channelCount := 0

  TrackBuilder newTrack (Int? channel:=null) {
    if (channel==null) {
      channel = channelCount++
      if (channel==PERC_CHANNEL) channel = channelCount++
    }
    if (channel<0 || channel>=MAX_CHANNELS) {
      throw Err("Invalid channel Number "+channel)
    }
    tbuilder := TrackBuilder(sequence.createTrack,channel)
    trackBuilders.add(tbuilder)
    return tbuilder
  }


  Int maxTicks() {
    trackBuilders.max |x,y|{x.timer<=>y.timer}.timer
  }

  Int sync() {
    max := maxTicks
    trackBuilders.each { it.timer = max }
    return max
  }

  Int tempo := DEFAULT_TEMPO {
    set {
      if (it<1) { throw Err("Invalid BPM value: "+it) }
      sync()
      // Microseconds per minute divided by BPM value
      mpq := 60000000 / it
      data := Int[3]
      data[0] = mpq.shiftr(16).and(0xFF)
      data[1] = mpq.shiftr(8).and(0xFF)
      data[2] = mpq.and(0xFF)
      trackBuilders[0].addMeta(META_SET_TEMPO, data)
      &tempo = it
    }
  }

  Void close(Int gracePeriod:=WHOLE) {
    sync
    trackBuilders.each { it.close(gracePeriod) }
  }

  Void play() {
    out := Env.cur.out
    sequencer := MidiSystem.getSequencer
    sequencer.open
    try {
      sequencer.setSequence(sequence)
      sequencer.start
      while (sequencer.isRunning) {
        out.print("<"+sequencer.getMicrosecondPosition()/1000+">")
        out.flush
        try {
          Thread.sleep(1000)
        } catch (Err e) {
          echo(e)
        }
      }
    } finally {
      sequencer.close
    }
    out.printLine()
  }
}



class TrackBuilder
{
  private Track track
  private Int channel

  Int timer

  internal new make(Track tr, Int ch) {
    track = tr
    channel = ch
  }

  Void advance(Int time) { timer += time }


  Void addMessage(MidiMessage message) {
    track.add(MidiEvent(message,timer))
  }

  Void addShort(Int command, Int data1:=0, Int data2:=0) {
    sm := ShortMessage()
    sm.setMessage(command,channel,data1,data2)
    addMessage(sm)
  }

  Void addMeta(Int type, Int[] data:=[,]) {
    bytes := makeBytes(data)
    mm := MetaMessage()
    mm.setMessage(type,bytes,bytes.size)
    addMessage(mm)
  }

  Void addSysex(Int status, Int[] data:=[,]) {
    bytes := makeBytes(data)
    sm := SysexMessage()
    sm.setMessage(status,bytes,bytes.size)
    addMessage(sm)
  }

  Void noteOn(Int pitch, Int velocity) {
    addShort(ShortMessage.NOTE_ON, pitch, velocity)
  }

  Void noteOff(Int pitch, Int decay) {
    addShort(ShortMessage.NOTE_OFF, pitch, decay)
  }

  Void programmChange(Int instrument) {
    addShort(ShortMessage.PROGRAM_CHANGE,instrument)
  }

  Void pitchBend (Float bend:=0f) {
    value := 8192 + (bend*4096f).toInt
    lsb := value % 128
    msb := value / 128
    addShort (ShortMessage.PITCH_BEND,lsb,msb)
  }

  Void close(Int gracePeriod:=0) {
    // Add EOT message
    advance(gracePeriod)
    addMeta(SequenceBuilder.META_END_OF_TRACK)
  }


  private static ByteArray makeBytes (Int[] array) {
    size := array.size
    bytes := ByteArray(size)
    for (i:=0; i<size; i++) { bytes[i] = array[i] }
    return bytes
  }

}